package com.Java.train.business.service.Impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.NumberUtil;
import com.Java.train.business.entity.DailyTrainSeat;
import com.Java.train.business.entity.DailyTrainStation;
import com.Java.train.business.enums.RedisKeyPreEnum;
import com.Java.train.business.mapper.cust.SkTokenCountCust;
import com.Java.train.business.service.daily.DailyTrainSeatService;
import com.Java.train.business.service.daily.DailyTrainStationService;
import com.Java.train.common.response.CommonResp;
import com.Java.train.business.entity.DTO.SkTokenDTO;
import com.Java.train.business.req.SkTokenQueryReq;
import com.Java.train.common.exception.BusniessException;
import com.Java.train.common.exception.BusniessExceptionEnum;
import com.Java.train.common.response.PageResp;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.Java.train.business.entity.SkToken;
import com.Java.train.business.mapper.SkTokenMapper;
import com.Java.train.business.service.SkTokenService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import com.Java.train.common.context.LoginMemberContext;
import com.Java.train.common.utils.SnowFlakeUtil;
import org.springframework.beans.factory.annotation.Autowired;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import org.springframework.util.CollectionUtils;
import cn.hutool.core.date.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.security.auth.login.LoginContext;
import java.util.Date;
import java.util.List;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

import static com.Java.train.business.enums.RedisKeyPreEnum.SK_TOKEN;
import static com.Java.train.business.enums.RedisKeyPreEnum.SK_TOKEN_COUNT;

/**
* 秒杀令牌 服务实现类
*
* @author luomoxingchen
* @since 2024-10-19
*/
@Service
public class SkTokenServiceImpl extends ServiceImpl<SkTokenMapper, SkToken> implements SkTokenService {

    private static final Logger LOG = LoggerFactory.getLogger(SkTokenService.class);

    @Autowired
    SkTokenMapper skTokenMapper;
    @Resource
    DailyTrainSeatService dailyTrainSeatService;
    @Resource
    DailyTrainStationService dailyTrainStationService;
    @Resource
    StringRedisTemplate stringRedisTemplate;
    @Resource
    SkTokenCountCust skTokenCountCust;

    @Override
    public PageResp<SkTokenDTO> queryList(SkTokenQueryReq skTokenQueryReq){
        // 1.获取当前用户id信息
        //        Long id = LoginMemberContext.getId();
        // 2.查询数据库中是否存在当前信息（id）
        LOG.info("查询页码：{}", skTokenQueryReq.getPage());
        LOG.info("每页条数：{}", skTokenQueryReq.getSize());
        PageHelper.startPage(skTokenQueryReq.getPage(),skTokenQueryReq.getSize());
        List<SkToken> skTokens = list();
        if(skTokens == null || CollUtil.isEmpty(skTokens)){
            throw new BusniessException(BusniessExceptionEnum.MEMBER_PASSENGER_NOT_EXITS);
        }

        // 3 分析分页信息
        // 3.1 获取分页总条数和页数
        PageInfo<SkToken> pageInfo = new PageInfo<>(skTokens);
        LOG.info("总行数：{}", pageInfo.getTotal());
        LOG.info("总页数：{}", pageInfo.getPages());

        // 3.2 转为封装后的返回值并封装到分页返回值中
        List<SkTokenDTO> list = BeanUtil.copyToList(skTokens, SkTokenDTO.class);
        PageResp<SkTokenDTO> pageResp = new PageResp<>();
        pageResp.setTotal(pageInfo.getTotal());
        pageResp.setList(list);
        // 4.返回查询到的信息
        return pageResp;
    }

    @Override
    public CommonResp<Object> modify(SkToken skToken) {
        Long id = skToken.getId();
        DateTime dateTime = DateTime.now();
        if(ObjectUtil.isNull(id)){
            skToken.setId(SnowFlakeUtil.getSnowFlakeNextId());
            skToken.setCreateTime(dateTime);
            skToken.setUpdateTime(dateTime);
            boolean save = save(skToken);
            return new CommonResp<>(save);
        } else {
            skToken.setUpdateTime(dateTime);
            boolean update = updateById(skToken);
            return new CommonResp<>(update);
        }
    }


     /**
      * @Description: 生成每日列车的令牌数量
      * @param
      * @return
      */
    @Override
    public void genDaily(Date date, String trainCode) {
        // 1.删除所有每日列车的所有令牌信息
        LambdaQueryWrapper<SkToken> wrapper = new LambdaQueryWrapper<>();
        remove(wrapper.eq(SkToken::getDate, date).eq(SkToken::getTrainCode, trainCode));
        // 2.获取所有列车的车座个数和车站个数，因此令牌最多的情况是所有车座*所有车站数量
        // 2.1 获取该列车当天所有车座数
        LambdaQueryWrapper<DailyTrainSeat> seatWrapper = new LambdaQueryWrapper<>();
        long seatCount = dailyTrainSeatService.count(seatWrapper.eq(DailyTrainSeat::getDate, date).eq(DailyTrainSeat::getTrainCode, trainCode));

        // 2.2 获取该列车当天所有车站数
        LambdaQueryWrapper<DailyTrainStation> stationWrapper = new LambdaQueryWrapper<>();
        long stationCount = dailyTrainStationService.count(stationWrapper.eq(DailyTrainStation::getDate, date).eq(DailyTrainStation::getTrainCode, trainCode));

        // 3.计算令牌数量3/4 * 座位数 * 站数，并初始化SkToken信息
        DateTime now = DateTime.now();
        long count = seatCount * stationCount ;
        SkToken skToken = new SkToken();
        skToken.setId(SnowFlakeUtil.getSnowFlakeNextId());
        skToken.setDate(date);
        skToken.setTrainCode(trainCode);
        skToken.setCount((int) count);
        skToken.setCreateTime(now);
        skToken.setUpdateTime(now);
        save(skToken);

    }

    public boolean vaildSkToken(Date date, String trainCode){
        // 1.分布式获取令牌，只有有令牌才能进行购票，并给出限制购票运行令牌的时间，防止同一人短时间大量抢票，手动限流（限个人）
        Long memberId = LoginMemberContext.getId();
        String tokenKey = SK_TOKEN + date.toString() + '-' +trainCode + memberId;
        Boolean tokenIfAbsent = stringRedisTemplate.opsForValue().setIfAbsent(tokenKey, tokenKey, 5, TimeUnit.SECONDS);
        if(tokenIfAbsent){
            LOG.info("恭喜，获取到令牌购票权限");
        } else {
            LOG.info("抱歉，没获取到令牌购票权限");
            return false;
        }

        // 2.从缓存中获取令牌的个数，设置每五（可以自定义）个人获取令牌就更新一次数据库
        String skTokenCountKey = RedisKeyPreEnum.SK_TOKEN_COUNT + "-" + DateUtil.formatDate(date) + "-" + trainCode;
        Object skTokenCount = stringRedisTemplate.opsForValue().get(skTokenCountKey);
        if(skTokenCount != null){
            // 2.1 缓存中有令牌剩余
            LOG.info("缓存中有该车次的令牌大闸为：{}", skTokenCount);
            Long count = stringRedisTemplate.opsForValue().decrement(skTokenCountKey, 1);
            if(count < 0L){
                LOG.info("缓存令牌没剩的了");
                return false;
            } else {
                // 2.1.2 重置一分钟缓存时间，代表当前可能还有人来买票
                stringRedisTemplate.expire(skTokenCountKey, 60, TimeUnit.SECONDS);
                // 2.1.3 每五个令牌消耗存储一下数据库
                if(count % 5 == 0){
                    int decreaseCount = count.intValue();
                    skTokenCountCust.decrease(date, trainCode, decreaseCount);
                }
                return true;
            }
        } else {
            // 2.2 缓存中没有令牌
            // 查数据库中的锁数量
            LambdaQueryWrapper<SkToken> wrapper = new LambdaQueryWrapper<>();
            SkToken skToken = getOne(wrapper.eq(SkToken::getDate, date).eq(SkToken::getTrainCode, trainCode));
            if(ObjectUtil.isEmpty(skToken)){
                LOG.info("该车次令牌不存在");
                return false;
            }
            Integer count = skToken.getCount();
            if(count <= 0){
                LOG.info("该日期为【{}】车次【{}】的令牌数为空", date, trainCode);
                return false;
            }
            // 令牌有余
            count--;
            skToken.setCount(count);
            // 存入缓存
            stringRedisTemplate.opsForValue().set(skTokenCountKey, String.valueOf(count), 60, TimeUnit.SECONDS);
//            updateById(skToken);
            return true;
        }
    }

}


